############################################################################
# Copyright (c) Kumo Ins                                                   #
# Distributed under the terms of the BSD 3-Clause License.                 #
#                                                                          #
# The full license is in the file LICENSE, distributed with this software. #
############################################################################

include(CheckCXXCompilerFlag)
include(CheckCXXSourceRuns)
INCLUDE(CheckCXXSourceRuns)

SET(SSE1_CODE "
  #include <xmmintrin.h>

  int main()
  {
    __m128 a;
    float vals[4] = {0,0,0,0};
    a = _mm_loadu_ps(vals);  // SSE1
    return 0;
  }")

SET(SSE2_CODE "
  #include <emmintrin.h>

  int main()
  {
    __m128d a;
    double vals[2] = {0,0};
    a = _mm_loadu_pd(vals);  // SSE2
    return 0;
  }")

SET(SSE3_CODE "
#include <pmmintrin.h>
int main() {
    __m128 u, v;
    u = _mm_set1_ps(0.0f);
    v = _mm_moveldup_ps(u); // SSE3
    return 0;
}")

SET(SSSE3_CODE "
  #include <tmmintrin.h>
  const double v = 0;
  int main() {
    __m128i a = _mm_setzero_si128();
    __m128i b = _mm_abs_epi32(a); // SSSE3
    return 0;
  }")

SET(SSE4_1_CODE "
  #include <smmintrin.h>

  int main ()
  {
    __m128i a  = _mm_setzero_si128();
    __m128i b =  _mm_setzero_si128();
    __m128i res = _mm_max_epi8(a, b); // SSE4_1
    (void)res;
    return 0;
  }
")

SET(SSE4_2_CODE "
  #include <nmmintrin.h>

  int main()
  {
    __m128i a = {0,0,0,0}, b = {0,0,0,0}, c = {0,0,0,0};
    c = _mm_cmpgt_epi64(a, b);  // SSE4_2
    return 0;
  }
")


MACRO(CHECK_SSE lang type flags)
    SET(__FLAG_I 1)
    SET(CMAKE_REQUIRED_FLAGS_SAVE ${CMAKE_REQUIRED_FLAGS})
    FOREACH (__FLAG ${flags})
        IF (NOT ${lang}_${type}_FOUND)
            SET(CMAKE_REQUIRED_FLAGS ${__FLAG})
            CHECK_CXX_SOURCE_RUNS("${${type}_CODE}" ${lang}_HAS_${type}_${__FLAG_I})
            IF (${lang}_HAS_${type}_${__FLAG_I})
                SET(${lang}_${type}_FOUND TRUE)
            ENDIF ()
            MATH(EXPR __FLAG_I "${__FLAG_I}+1")
        ENDIF ()
    ENDFOREACH ()
    SET(CMAKE_REQUIRED_FLAGS ${CMAKE_REQUIRED_FLAGS_SAVE})

    IF (NOT ${lang}_${type}_FOUND)
        SET(${lang}_${type}_FOUND FALSE)
    ENDIF ()
    message(STATUS "${lang}_${type}_FOUND ${${lang}_${type}_FOUND}")
    MARK_AS_ADVANCED(${lang}_${type}_FOUND ${lang}_${type}_FLAGS)

ENDMACRO()

SET(AVX_CODE "
#if !defined __AVX__ // MSVC supports this flag since MSVS 2013
#error \"__AVX__ define is missing\"
#endif
#include <immintrin.h>
void test()
{
    __m256 a = _mm256_set1_ps(0.0f);
}
int main() { return 0; }")

SET(AVX2_CODE "
#if !defined __AVX2__ // MSVC supports this flag since MSVS 2013
#error \"__AVX2__ define is missing\"
#endif
#include <immintrin.h>
void test()
{
    int data[8] = {0,0,0,0, 0,0,0,0};
    __m256i a = _mm256_loadu_si256((const __m256i *)data);
    __m256i b = _mm256_bslli_epi128(a, 1);  // available in GCC 4.9.3+
}
int main() { return 0; }")

SET(AVX512_CODE "
#if defined __AVX512__ || defined __AVX512F__
#include <immintrin.h>
void test()
{
    __m512i zmm = _mm512_setzero_si512();
#if defined __GNUC__ && defined __x86_64__
    asm volatile (\"\" : : : \"zmm16\", \"zmm17\", \"zmm18\", \"zmm19\");
#endif
}
#else
#error \"AVX512 is not supported\"
#endif
int main() { return 0; }")


MACRO(CHECK_AVX type flags)
    SET(__FLAG_I 1)
    SET(CMAKE_REQUIRED_FLAGS_SAVE ${CMAKE_REQUIRED_FLAGS})
    FOREACH (__FLAG ${flags})
        IF (NOT CXX_${type}_FOUND)
            SET(CMAKE_REQUIRED_FLAGS ${__FLAG})
            CHECK_CXX_SOURCE_RUNS("${${type}_CODE}" CXX_HAS_${type}_${__FLAG_I})
            IF (CXX_HAS_${type}_${__FLAG_I})
                SET(CXX_${type}_FOUND TRUE)
            ENDIF ()
            MATH(EXPR __FLAG_I "${__FLAG_I}+1")
        ENDIF ()
    ENDFOREACH ()
    SET(CMAKE_REQUIRED_FLAGS ${CMAKE_REQUIRED_FLAGS_SAVE})

    IF (NOT CXX_${type}_FOUND)
        SET(CXX_${type}_FOUND FALSE)
    ENDIF ()

    MARK_AS_ADVANCED(CXX_${type}_FOUND CXX_${type}_FLAGS)

ENDMACRO()
if ("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "x86_64" OR "${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "AMD64")
    CHECK_AVX("AVX" ";-mavx;/arch:AVX")
    CHECK_AVX("AVX2" ";-mavx2;/arch:AVX2")
    CHECK_AVX("AVX512" ";-mavx512;/arch:AVX512")
    CHECK_SSE(CXX "SSE1" ";-msse;/arch:SSE")
    CHECK_SSE(CXX "SSE2" ";-msse2;/arch:SSE2")
    CHECK_SSE(CXX "SSE3" ";-msse3;/arch:SSE3")
    CHECK_SSE(CXX "SSSE3" ";-mssse3;/arch:SSSE3")
    CHECK_SSE(CXX "SSE4_1" ";-msse4.1;-msse4;/arch:SSE4")
    CHECK_SSE(CXX "SSE4_2" ";-msse4.2;-msse4;/arch:SSE4")
elseif ("${CMAKE_SYSTEM_PROCESSOR}" MATCHES "arm.*|aarch64")
    IF (CMAKE_SYSTEM_NAME MATCHES "Linux")
        execute_process(COMMAND cat /proc/cpuinfo OUTPUT_VARIABLE CPUINFO)

        #neon instruction can be found on the majority part of modern ARM processor
        STRING(REGEX REPLACE "^.*(neon).*$" "\\1" NEON_THERE ${CPUINFO})
        STRING(COMPARE EQUAL "neon" "${NEON_THERE}" NEON_TRUE)
        IF (NEON_TRUE)
            set(NEON_FOUND true BOOL "NEON available on host")
        ELSE ()
            set(NEON_FOUND false BOOL "NEON available on host")
        ENDIF ()

        #Find the processor type (for now OMAP3 or OMAP4)
        STRING(REGEX REPLACE "^.*(OMAP3).*$" "\\1" OMAP3_THERE ${CPUINFO})
        STRING(COMPARE EQUAL "OMAP3" "${OMAP3_THERE}" OMAP3_TRUE)
        IF (OMAP3_TRUE)
            set(CORTEXA8_FOUND true BOOL "OMAP3 available on host")
        ELSE ()
            set(CORTEXA8_FOUND false BOOL "OMAP3 available on host")
        ENDIF ()

        #Find the processor type (for now OMAP3 or OMAP4)
        STRING(REGEX REPLACE "^.*(OMAP4).*$" "\\1" OMAP4_THERE ${CPUINFO})
        STRING(COMPARE EQUAL "OMAP4" "${OMAP4_THERE}" OMAP4_TRUE)
        IF (OMAP4_TRUE)
            set(CORTEXA9_FOUND true BOOL "OMAP4 available on host")
        ELSE ()
            set(CORTEXA9_FOUND false BOOL "OMAP4 available on host")
        ENDIF ()

    ELSEIF (CMAKE_SYSTEM_NAME MATCHES "Darwin")
        execute_process(COMMAND /usr/sbin/sysctl -n machdep.cpu.features OUTPUT_VARIABLE CPUINFO)

        #neon instruction can be found on the majority part of modern ARM processor
        STRING(REGEX REPLACE "^.*(neon).*$" "\\1" NEON_THERE ${CPUINFO})
        STRING(COMPARE EQUAL "neon" "${NEON_THERE}" NEON_TRUE)
        IF (NEON_TRUE)
            set(NEON_FOUND true BOOL "NEON available on host")
        ELSE ()
            set(NEON_FOUND false BOOL "NEON available on host")
        ENDIF ()

    ELSEIF (CMAKE_SYSTEM_NAME MATCHES "Windows")
        # TODO
        set(CORTEXA8_FOUND false BOOL "OMAP3 not available on host")
        set(CORTEXA9_FOUND false BOOL "OMAP4 not available on host")
        set(NEON_FOUND false BOOL "NEON not available on host")
    ELSE (CMAKE_SYSTEM_NAME MATCHES "Linux")
        set(CORTEXA8_FOUND false BOOL "OMAP3 not available on host")
        set(CORTEXA9_FOUND false BOOL "OMAP4 not available on host")
        set(NEON_FOUND false BOOL "NEON not available on host")
    ENDIF ()

    if (NOT NEON_FOUND)
        MESSAGE(STATUS "Could not find hardware support for NEON on this machine.")
    endif ()
    if (NOT CORTEXA8_FOUND)
        MESSAGE(STATUS "No OMAP3 processor on this on this machine.")
    endif ()
    if (NOT CORTEXA9_FOUND)
        MESSAGE(STATUS "No OMAP4 processor on this on this machine.")
    endif ()
    mark_as_advanced(NEON_FOUND)
endif ()

option(KSIMD_ENABLE_ARCH "enable cpu instruct" ON)

list(APPEND KSIMD_BASE__ARM32_FLAGS
        "-mfpu=neon"
        )

list(APPEND KSIMD_BASE__ARM64_FLAGS
        "-march=armv8-a+crypto"
        )

list(APPEND KSIMD_BASE__MSVC_X64_FLAGS
        )

list(APPEND KSIMD_BASE_X64_FLAGS
        "-maes"
        "-msse4.1"
        )

if("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "x86_64" OR "${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "AMD64")
    if (MSVC)
        set(KSIMD_BASE_ARCH_OPTIONS "${KSIMD_BASE__MSVC_X64_FLAGS}")
    else()
        set(KSIMD_BASE_ARCH_OPTIONS "${KSIMD_BASE_X64_FLAGS}")
    endif()
elseif("${CMAKE_SYSTEM_PROCESSOR}" MATCHES "arm.*|aarch64")
    if ("${CMAKE_SIZEOF_VOID_P}" STREQUAL "8")
        set(KSIMD_BASE_ARCH_OPTIONS "${KSIMD_BASE__ARM64_FLAGS}")
    elseif("${CMAKE_SIZEOF_VOID_P}" STREQUAL "4")
        set(KSIMD_BASE_ARCH_OPTIONS "${KSIMD_BASE__ARM32_FLAGS}")
    else()
        message(WARNING "Value of CMAKE_SIZEOF_VOID_P (${CMAKE_SIZEOF_VOID_P}) is not supported.")
    endif()
else()
    message(WARNING "Value of CMAKE_SYSTEM_PROCESSOR (${CMAKE_SYSTEM_PROCESSOR}) is unknown and cannot be used to set KSIMD_BASE_ARCH_OPTIONS")
    set(KSIMD_BASE_ARCH_OPTIONS "")
endif()

function(ksimd_check_description_length name description)
    foreach(description_line ${description})
        string(LENGTH ${description_line} line_length)
        if(${line_length} GREATER 80)
            message(FATAL_ERROR "description for ${name} contained a\n\
        line ${line_length} characters long!\n\
        (max is 80). Split it into more lines with semicolons")
        endif()
    endforeach()
endfunction()


macro(ksimd_define_option_string name description default)
    ksimd_check_description_length(${name} ${description})
    list(JOIN description "\n" multiline_description)

    set(${name}
            ${default}
            CACHE STRING "${multiline_description}")

    list(APPEND "KSIMD_COMPILE_OPTION_NAMES" ${name})
    set("${name}_OPTION_DESCRIPTION" ${description})
    set("${name}_OPTION_DEFAULT" "\"${default}\"")
    set("${name}_OPTION_TYPE" "string")
    set("${name}_OPTION_POSSIBLE_VALUES" ${ARGN})
    list(FIND ${name}_OPTION_POSSIBLE_VALUES "${default}" default_value_index)
    if(NOT ${default_value_index} EQUAL -1)
        list(REMOVE_AT ${name}_OPTION_POSSIBLE_VALUES ${default_value_index})
        list(PREPEND ${name}_OPTION_POSSIBLE_VALUES "${default}")
    endif()
    list(JOIN "${name}_OPTION_POSSIBLE_VALUES" "|" "${name}_OPTION_ENUM")
    if(NOT ("${${name}_OPTION_ENUM}" STREQUAL ""))
        set_property(CACHE ${name} PROPERTY STRINGS "${name}_OPTION_POSSIBLE_VALUES")
    endif()
endmacro()

ksimd_define_option_string(KSIMD_LEVEL
        "Compile-time SIMD optimization level"
        "DEFAULT" # default to SSE4_2 on x86, NEON on Arm, NONE otherwise
        "NONE"
        "SSE4_2"
        "AVX2"
        "AVX512"
        "DEFAULT")

set(KSIMD_ARCH_OPTION "")
if (KSIMD_ENABLE_ARCH)
    if(KSIMD_LEVEL STREQUAL "DEFAULT")
        set(KSIMD_LEVEL "AVX2")
    endif()
    if (CXX_AVX2_FOUND)
        message(STATUS "AVX2 SUPPORTED for CXX")
        set(AVX2_SUPPORTED true)
        set(HIGHEST_SIMD_SUPPORTED "AVX2")
        list(APPEND SSE_SUPPORTED_LIST ${AVX2_SUPPORTED})
    else ()
        set(AVX2_SUPPORTED false)
    endif ()

    if (CXX_AVX512_FOUND)
        message(STATUS "AVX512 SUPPORTED for C and CXX")
        set(AVX512_SUPPORTED true)
        set(HIGHEST_SIMD_SUPPORTED "AVX512")
        list(APPEND SSE_SUPPORTED_LIST ${AVX512_SUPPORTED})
    else ()
        set(AVX512_SUPPORTED false)
    endif ()
    message(STATUS ${KSIMD_LEVEL})
    set(KSIMD_ARCH_OPTION)

    if (CXX_SSE1_FOUND)
        message(STATUS "KSIMD SSE1 SELECTED")
        list(APPEND KSIMD_SSE1_SIMD_FLAGS "-msse")
        list(APPEND KSIMD_ARCH_OPTION ${KSIMD_SSE1_SIMD_FLAGS})
    endif ()

    if (CXX_SSE2_FOUND)
        message(STATUS "KSIMD SSE2 SELECTED")
        list(APPEND KSIMD_SSE2_SIMD_FLAGS "-msse2")
        list(APPEND KSIMD_ARCH_OPTION ${KSIMD_SSE2_SIMD_FLAGS})
    endif ()

    if (CXX_SSE3_FOUND)
        message(STATUS "KSIMD SSE3 SELECTED")
        list(APPEND KSIMD_SSE3_SIMD_FLAGS "-msse3")
        list(APPEND KSIMD_ARCH_OPTION ${KSIMD_SSE3_SIMD_FLAGS})
    endif ()

    if (CXX_SSSE3_FOUND)
        message(STATUS "KSIMD SSSE3 SELECTED")
        list(APPEND KSIMD_SSSE3_SIMD_FLAGS "-mssse3")
        list(APPEND KSIMD_ARCH_OPTION ${KSIMD_SSSE3_SIMD_FLAGS})
    endif ()
    message(STATUS "CXX_SSE4_1_FOUND ${CXX_SSE4_1_FOUND}")
    if (CXX_SSE4_1_FOUND)
        message(STATUS "KSIMD SSE4_1 SELECTED")
        list(APPEND KSIMD_SSE4_1_SIMD_FLAGS "-msse4.1")
        list(APPEND KSIMD_ARCH_OPTION ${KSIMD_SSE4_1_SIMD_FLAGS})
    endif ()

    if(KSIMD_LEVEL STREQUAL "SSE4_2" AND NOT CXX_SSE4_2_FOUND)
        message(FATAL_ERROR "build setting sse4_2 but cpu not support!")
    endif()
    if (CXX_SSE4_2_FOUND)
        message(STATUS "KSIMD SSE4_2 SELECTED")
        list(APPEND KSIMD_SSE4_2_SIMD_FLAGS "-msse4.2")
        list(APPEND KSIMD_ARCH_OPTION ${KSIMD_SSE4_2_SIMD_FLAGS})
    endif ()

    if(KSIMD_LEVEL STREQUAL "AVX2" AND NOT CXX_AVX_FOUND)
        message(FATAL_ERROR "build setting avx but cpu not support!")
    endif()

    if (CXX_AVX_FOUND)
        message(STATUS "KSIMD AVX SELECTED")
        list(APPEND KSIMD_AVX_SIMD_FLAGS "-mavx")
        list(APPEND KSIMD_ARCH_OPTION ${KSIMD_AVX_SIMD_FLAGS})
    endif ()

    if(KSIMD_LEVEL STREQUAL "AVX2" AND NOT CXX_AVX2_FOUND)
        message(FATAL_ERROR "build setting avx2 but cpu not support!")
    endif()
    if (CXX_AVX2_FOUND)
        message(STATUS "KSIMD AVX2 SELECTED")
        list(APPEND KSIMD_AVX2_SIMD_FLAGS "-mavx2" "-mfma")
        list(APPEND KSIMD_ARCH_OPTION ${KSIMD_AVX2_SIMD_FLAGS})
    endif ()

    if(KSIMD_LEVEL STREQUAL "AVX512" AND NOT CXX_AVX512_FOUND)
        message(FATAL_ERROR "build setting avx512 but cpu not support!")
    endif()

    if (CXX_AVX512_FOUND)
        message(STATUS "KSIMD AVX512 SELECTED")
        list(APPEND KSIMD_AVX512_SIMD_FLAGS "-mavx512f" "-mfma") # note that this is a bit platform specific
        list(APPEND KSIMD_ARCH_OPTION ${KSIMD_AVX512_SIMD_FLAGS}) # note that this is a bit platform specific
    endif ()
endif ()
list(APPEND KSIMD_ARCH_OPTION ${KSIMD_BASE_ARCH_OPTIONS})
list(REMOVE_DUPLICATES KSIMD_ARCH_OPTION)
MESSAGE(STATUS "KSIMD ARCH FLAGS ${KSIMD_ARCH_OPTION}")
